Esercitazione 3
Soluzioni passo-passo esercizi per casa
Esercizio 1.2: istruzioni stringa, soluzione passo-passo
Ricordiamo la traccia dell'esercizio:
1. Leggere messaggio da terminale.
2. Convertire le lettere minuscole in maiuscolo, usando le istruzioni stringa.
3. Stampare messaggio modificato.
Le istruzioni stringa sono un esempio di set di istruzioni specializzate, cioè istruzioni che non sono pensate per implementare algoritmi generici, ma sono invece pensate per fornire supporto hardware efficiente a uno specifico set di operazioni che alcuni algoritmi necessitano. Infatti, ci si può aspettare che tra due programmi equivalenti, uno scritto con sole istruzioni generali e l'altro scritto con istruzioni specializzate, il secondo sarà molto più performante del primo. Altri esempi comuni sono le istruzioni a supporto di crittografia, encoding e decoding di stream multimediali, e, più recentemente, neural networks.
Questi set di istruzioni sono però più "rigidi" delle istruzioni a uso generale. Ci impongono infatti dei modi specifici di organizzare dati e codice, perché questi devono essere compatibili con il modo in cui l'algoritmo eseguito da un'istruzione è implementato in hardware.
Nell'esercizio 1.1 abbiamo considerato due modi di scorrere i due array. Nel primo, che è quello che abbiamo scelto, si carica l'indirizzo di inizio del vettore, e si usa un altro registro come indice, usando l'indirizzazione con indice. Nel secondo, si usa un registro come puntatore alla cella corrente, inizializzato all'indirizzo di inizio del vettore e poi incrementato (della quantità giusta) per passare all'elemento successivo. In entrambi i casi, siamo liberi di usare i registri che vogliamo, per esempio non abbiamo nessun problema se scriviamo il programma di prima come segue:
lea msg_in, %eax
lea msg_out, %ebx
mov $0, %edx
loop:
movb (%eax, %edx), %cl
...
Infatti, usare esi
ed edi
come registri puntatori, ed ecx
come registro di indice, è del tutto opzionale.
Tutto questo cambia quando si vogliono usare istruzioni specializzate come le istruzioni stringa.
Queste ci impongono di usare esi
come puntatore al vettore sorgente, edi
come puntatore al vettore destinatario, eax
come registro dove scrivere o da cui leggere il valore da trasferire, ecx
come contatore delle ripetizioni da eseguire, etc.
Una volta scelte le istruzioni da usare, dobbiamo quindi assicurarci di seguire quanto imposto dall'istruzione.
Per questo esercizio siamo interessati alla lods
, che legge un valore dal vettore e ne sposta il puntatore allo step successivo, e la stos
, che scrive un valore nel vettore.
Partiamo dal riscrivere il punto_2
dell'esercizio 1.1 in modo da rendere l'algoritmo compatibile.
...
punto_2:
lea msg_in, %esi
lea msg_out, %edi
loop:
movb (%esi), %al
inc %esi
cmp $'a', %al
jb post_check
cmp $'z', %al
ja post_check
and $0xdf, %al
post_check:
movb %al, (%edi)
inc %edi
cmp $0x0d, %al
jne loop
...
Abbiamo dunque rimosso l'uso di ecx
come indice, e usiamo esi
ed edi
come puntatori.
Il fatto di usare la inc
è legato alla dimensione dei dati, cioè 1 byte.
Dovremmo invece scrivere add $2, %esi
o add $4, %esi
per dati su 2 o 4 byte.
Altra nota è che incrementiamo i puntatori, anziché decrementarli, perché stiamo eseguendo l'operazione da sinistra verso destra.
Siamo pronti adesso a sostituire le istruzioni evidenziate con delle istruzioni stringa. Il sorgente finale è scaricabile qui.
...
punto_2:
lea msg_in, %esi
lea msg_out, %edi
cld
loop:
lodsb
cmp $'a', %al
jb post_check
cmp $'z', %al
ja post_check
and $0xdf, %al
post_check:
stosb
cmp $0x0d, %al
jne loop
...
L'istruzione cld
serve a impostare a 0 il flag di direzione, che serve a indicare alle istruzioni stringa se andare da sinistra verso destra o il contrario.
Dato che tutti i registri sono impliciti, dobbiamo sempre specificare la dimensione delle istruzioni, in questo caso b
.
Come esercizio, può essere interessante osservare con il debugger l'evoluzione dei registri, osservando come si eseguono più operazioni con una sola istruzione.
Esercizio 1.6: esercizio di debugging, soluzione passo-passo
Ricordiamo la traccia dell'esercizio:
Scrivere un programma che, a partire dalla sezione
.data
che segue (e scaricabile qui), conta e stampa il numero di occorrenze dinumero
inarray
..include "./files/utility.s"
.data
array: .word 1, 256, 256, 512, 42, 2048, 1024, 1, 0
array_len: .long 9
numero: .word 1
Questa è invece la soluzione proposta dall'esercizio:
.include "./files/utility.s"
.data
array: .word 1, 256, 256, 512, 42, 2048, 1024, 1, 0
array_len: .long 9
numero: .word 1
.text
_main:
nop
mov $0, %cl
mov numero, %ax
mov $0, %esi
comp:
cmp array_len, %esi
je fine
cmpw array(%esi), %ax
jne poi
inc %cl
poi:
inc %esi
jmp comp
fine:
mov %cl, %al
call outdecimal_byte
ret
Come prima cosa, cerchiamo di capire, a grandi linee, cosa cerca di fare questo programma.
Notiamo l'uso di %cl
: dall'inizializzazione a riga 12, l'incremento condizionato a righe 19-21, e la stampa a righe 28-29, si evince che %cl
è usato come contatore dei successi, ossia di quante volte è stato trovato numero
in array
.
Notiamo che %ax
viene inizializzato con numero
e, prima della stampa, mai aggiornato.
Infine, %esi
viene inizializzato a 0 e incrementato a fine di ogni ciclo, confrontandolo con array_len
per determinare quando uscire dal loop.
Infine, a riga 19 notiamo il confronto tra un valore di array
, indicizzato con %esi
, e %ax
, che contiene numero
.
Si ricostruisce quindi questa logica: scorro valore per valore array
, indicizzandolo con %esi
, e lo confronto con numero
, che è appoggiato su %ax
(perché il confronto tra due valori in memoria non è possibile con cmp
). Utilizzo %cl
come contatore dei successi, e alla fine dello scorrimento ne stampo il valore.
Fin qui nessuna sorpresa, il programma sembra seguire lo schema che si seguirebbe con un normale programma in C:
int cl = 0;
for(int esi = 0; esi < array_len; esi++){
if(array[esi] == numero)
cl++;
}
Proviamo ad eseguire il programma: ci si aspetta che stampi 2. Invece, stampa 3. Dobbiamo passare al debugger.
Quello che ci conviene guardare è quello che succede ad ogni loop, in particolare alla riga 19, dove la cmpw
confronta un valore di array
con %ax
, che contiene numero
.
Però, la cmpw
utilizza un indirizzamento complesso che, come descritto nella documentazione, richiede una sintassi più complicata nel debugger.
Cambio quindi quella istruzione in una serie equivalente che sia più facile da osservare col debugger.
.include "./files/utility.s"
.data
array: .word 1, 256, 256, 512, 42, 2048, 1024, 1, 0
array_len: .long 9
numero: .word 1
.text
_main:
nop
mov $0, %cl
mov numero, %ax
mov $0, %esi
comp:
cmp array_len, %esi
je fine
movw array(%esi), %bx
cmpw %bx, %ax
jne poi
inc %cl
poi:
inc %esi
jmp comp
fine:
mov %cl, %al
call outdecimal_byte
ret
Assemblo, avvio il debugger, e setto un breakpoint alla riga 20 con break 20
.
Lascio girare il programma con continue
, che quasi immediatamente raggiunge la riga 20 e si ferma.
Ricordiamo che il debugger si ferma prima di eseguire una istruzione.
Vediamo lo stato dei registri, con i r ax bx cl esi
(mostra solo quelli che ci interessano).
(gdb) i r ax bx cl esi
ax 0x1 1
bx 0x1 1
cl 0x0 0
esi 0x0 0
Fin qui, tutto come ci si aspetta: %ax
che contiene numero
, %bx
contiene il numero alla prima cella di array
, i due contatori %cl
e %esi
sono a 0.
Facciamo step
per vedere l'esito del confronto: dopo la riga 21 l'esecuzione giunge alla riga 22, indicando che il salto non è stato fatto perché la jne
è stata eseguita dopo un confronto tra valori uguali.
Continuiamo con step
controllando che il comportamento sia quello atteso, fino a giungere di nuovo alla riga 20.
(gdb) i r ax bx cl esi
ax 0x1 1
bx 0x0 0
cl 0x1 1
esi 0x1 1
Qui abbiamo la prima sorpresa. In %bx
troviamo 0, ma il secondo valore di array
è 256.
Se continuiamo, vediamo che 256 compare come terzo valore, poi 1 come quarto, poi 256 come quinto.
Abbiamo quindi dei valori aggiuntivi che compaiono durante lo scorrimento del vettore ma che non sono presenti nell'allocazione a riga 4.
Continuando ancora, vediamo che i 9 valori coperti dal programma non sono affatto tutti e 9 quelli a riga 4, e che effettivamente il valore 1 compare 3 volte.
Con questo, abbiamo intanto confinato il problema: la logica di confronto e conteggio funziona, il problema è nella lettura di valori da array
.
Per capire cosa sta succedendo, dobbiamo ricordare come si comporta l'allocazione in memoria di valori su più byte: abbiamo infatti a che fare con word, composte da 2 byte ciascuna, e ciascun indirizzo in memoria corrisponde a una locazione di un solo byte.
L'architettura x86 è little-endian, che significa little end first, un riferimento a I viaggi di Gulliver. Questo si traduce nel fatto che quando un valore di byte viene salvato in memoria a partire dall'indirizzo , il byte meno significativo del valore viene salvato in , il secondo meno significativo in , e così via fino al più significativo in .
Possiamo quindi immaginare così il nostro array
in memoria.

Layout di array
in memoria.
La lettura di una word dalla memoria funziona quindi così: dato l'indirizzo , vengono letti i due byte agli indirizzi e e contatenati nell'ordine .
Una istruzione come movw a, %bx
, quindi, salverà il contenuto di in %bh
e il contenuto di in %bl
.
Per la lettura di più word consecutive, dobbiamo assicurarci di incrementare l'indirizzo di 2 alla volta: come mostrato in figura, il secondo valore è memorizzato a partire da , il terzo da , e così via.
Tornando però al codice dell'esercizio, questo non succede:
comp:
cmp array_len, %esi
je fine
movw array(%esi), %bx
cmpw %bx, %ax
jne poi
inc %cl
poi:
inc %esi
jmp comp
Ecco quindi spiegato cosa legge il programma dalla memoria:
quando alla seconda iterazione si esegue movb array(%esi), %bx
, con %esi
che vale 1, si sta leggendo un valore composto dal byte meno significativo del secondo valore concatenato con il byte più significativo del primo.
Questo valore è del tutto estraneo e privo di senso se confrontato con array
così come è stato dichiarato e allocato, ma nell'eseguire le istruzioni il processore non sa e non controlla niente di tutto ciò.

Lettura erronea di array
: sbagliando l'incremento dell'indirizzo, leggiamo dei byte senza alcuna relazione fra loro dalla memoria e li interpretiamo come parti di una word.
Abbiamo due strade per correggere questo errore.
Il primo approccio è quello di incrementare %esi
di 2 alla volta, così che l'indirizzamento array(%esi)
risulti corretto.
Con questo schema però %esi
dovrà assumere i valori , cosa che lo rende non più un contatore confrontabile direttamente con array_len
come fatto a riga 17.
Si dovrà gestire tale confronto in altro modo, per esempio usando un registro separato come contatore.
La seconda strada è quella di usare il fattore di scala dell'indirizzamento, che è pensato proprio per essere utilizzato in casi come questo.
Infatti, array(, %esi, 2)
calcolerà l'indirizzo